interface Window {
  __Wab_Listener__?: any;
  __Wab_Legacy__?: any;
  // @ts-ignore
  Wab: Wabo;
  WabCookiesDescriptor?: PropertyDescriptor;
  WabFetch: typeof fetch;
  WabXMLHttpRequest: any;
}

interface PatchedXMLHttpRequestConstructor extends XMLHttpRequest {
  new (): XMLHttpRequest;
}

const WAB_HTTP_INTERCEPTOR = "/_Wab_http_interceptor_";
const WAB_HTTP_INTERCEPTOR_URL_PARAM = "u";

export function startPatch() {
  /**
   * Patches the cookie behavior for the given window.
   */
  // @ts-ignore
  window.WabCookiesDescriptor =
    Object.getOwnPropertyDescriptor(Document.prototype, "cookie") ||
    Object.getOwnPropertyDescriptor(HTMLDocument.prototype, "cookie");
  Object.defineProperty(document, "cookie", {
    get: function () {
      // return original document.cookie since Android does not support filtering of `httpOnly` cookies
      // @ts-ignore
      return window.WabCookiesDescriptor?.get?.call(document) ?? "";
    },
    set: function (val) {
      const cookiePairs = val.split(";");
      const domainSection = val.toLowerCase().split("domain=")[1];
      const domain =
        cookiePairs.length > 1 &&
        domainSection != null &&
        domainSection.length > 0
          ? domainSection.split(";")[0].trim()
          : "";
      // @ts-ignore
      Wab.callFunction({ funcName: "setCookie", params: { domain, val } });
    },
  });

  __implXMR();
}

/**
 * Reads a file and returns its contents as a base64-encoded string.
 */
function readFileAsBase64(file: File): Promise<string> {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onloadend = () => {
      const data = reader.result;
      if (data) {
        if (data instanceof String) {
          resolve(btoa(data.toString()));
        } else if (data instanceof ArrayBuffer) {
          const arr = new Uint8Array(data);
          let str = "";
          for (let i = 0; i < arr.length; i++) {
            str += String.fromCharCode(arr[i]);
          }
          resolve(btoa(str));
        }
      }
    };
    reader.onerror = reject;
    reader.readAsText(file);
  });
}

export type FormDataItem = {
  key: string;
  value: string;
  type: "string" | "base64File";
  contentType?: string;
  fileName?: string;
};
/**
 * Converts a FormData object to an array of key-value pairs.
 */
async function convertFormData(formData: FormData): Promise<FormDataItem[]> {
  const newFormData: FormDataItem[] = [];
  const entries = formData.entries();
  let pair;
  while (!(pair = entries.next()).done) {
    const [key, value] = pair.value;
    // @ts-ignore
    if (value instanceof File) {
      const base64File = await readFileAsBase64(value);
      newFormData.push({
        key,
        value: base64File,
        type: "base64File",
        contentType: value.type,
        fileName: value.name,
      });
    } else {
      newFormData.push({ key, value, type: "string" });
    }
  }
  return newFormData;
}
export type BodyType = {
  data:
    | Blob
    | ArrayBufferView<ArrayBufferLike>
    | string
    | FormDataItem[]
    | Uint8Array
    | File
    | null
    | ArrayBuffer;
  type: "json" | "text" | "formData" | "image" | "binary" | "file" | string;
  headers?: Record<string, string>;
};
/**
 * Converts the body to a format that can be sent over the network.
 */
async function convertBody(
  body?: BodyInit | Uint8Array | File | XMLHttpRequestBodyInit,
  contentType?:
    | "application/json"
    | "multipart/form-data"
    | "application/octet-stream"
    | string
): Promise<BodyType> {
  if (!body) {
    throw "IllegalArgument: body is null!";
  }
  if (body instanceof ReadableStream || body instanceof Uint8Array) {
    let encodedData: AllowSharedBufferSource | undefined;
    if (body instanceof ReadableStream) {
      const reader = body.getReader();
      const chunks: Uint8Array[] = [];
      while (true) {
        const { done, value } = await reader.read();
        if (done) break;
        chunks.push(value as Uint8Array);
      }
      const concatenated = new Uint8Array(
        chunks.reduce((acc, chunk) => acc + chunk.length, 0)
      );
      let position = 0;
      for (const chunk of chunks) {
        concatenated.set(chunk, position);
        position += chunk.length;
      }
      encodedData = concatenated;
    } else {
      encodedData = body;
    }
    let data = new TextDecoder().decode(encodedData);
    let type;
    if (contentType === "application/json") {
      try {
        data = JSON.parse(data);
      } catch (ignored) {
        // ignore TODO: log error
      }
      type = "json";
    } else if (contentType === "multipart/form-data") {
      type = "formData";
    } else if (
      contentType === null || contentType === void 0
        ? void 0
        : contentType.startsWith("image")
    ) {
      type = "image";
    } else if (contentType === "application/octet-stream") {
      type = "binary";
    } else {
      type = "text";
    }
    return {
      data,
      type,
      headers: { "Content-Type": contentType || "application/octet-stream" },
    };
  } else if (body instanceof URLSearchParams) {
    return {
      data: body.toString(),
      type: "text",
      headers: {
        "Content-Type": contentType || "application/x-www-form-urlencoded",
      },
    };
  } else if (body instanceof FormData) {
    const formData = await convertFormData(body);
    return {
      data: formData,
      type: "formData",
      headers: { "Content-Type": contentType || "multipart/form-data" },
    };
  } else if (body instanceof File) {
    const fileData = await readFileAsBase64(body);
    return {
      data: fileData,
      type: "file",
      headers: { "Content-Type": body.type },
    };
  }
  return {
    data: body,
    type: "json",
    headers: { "Content-Type": contentType || "application/json" },
  };
}

/**
 * Checks if the given URL is relative or a proxy URL.
 */
function isRelativeOrProxyUrl(url: string): boolean {
  return (
    !url ||
    !(url.startsWith("http:") || url.startsWith("https:")) ||
    url.indexOf(WAB_HTTP_INTERCEPTOR) > -1
  );
}

/**
 * Creates a proxy URL for the given URL.
 */
function createProxyUrl(url: string): string {
  if (isRelativeOrProxyUrl(url)) return url;
  // @ts-ignore
  const bridgeUrl = new URL(Wab.getValue({ name: "getServerUrl" }) ?? "");
  bridgeUrl.pathname = WAB_HTTP_INTERCEPTOR;
  bridgeUrl.searchParams.append(WAB_HTTP_INTERCEPTOR_URL_PARAM, url);
  return bridgeUrl.toString();
}
/**
 *  Converts a file path to a server URL.
 */
function convertFileSrcServerUrl(
  webviewServerUrl: string,
  filePath: string
): string {
  if (typeof filePath === "string") {
    if (filePath.startsWith("/")) {
      return webviewServerUrl + "/_Wab_file_" + filePath;
    } else if (filePath.startsWith("file://")) {
      return webviewServerUrl + filePath.replace("file://", "/_Wab_file_");
    } else if (filePath.startsWith("content://")) {
      return webviewServerUrl + filePath.replace("content:/", "/_Wab_content_");
    }
  }
  return filePath;
}

function __implXMR() {
  // patch fetch / XHR on Android/iOS
  // store original fetch & XHR functions
  Object.defineProperty(window, "WabFetch", {
    value: window.fetch,
    configurable: true,
    writable: true,
  });
  Object.defineProperty(window, "WabXMLHttpRequest", {
    value: window.XMLHttpRequest,
    configurable: true,
    writable: true,
  });
  window.XMLHttpRequest = function () {
    // @ts-ignore
    const xhr = new window.WabXMLHttpRequest();

    Object.defineProperties(xhr, {
      _headers: {
        value: {},
        writable: true,
      },
      _method: {
        value: "",
        writable: true,
      },
    });
    // @ts-ignore
    const prototype = window.WabXMLHttpRequest.prototype;

    const isProgressEventAvailable = () =>
      typeof ProgressEvent !== "undefined" &&
      ProgressEvent.prototype instanceof Event;

    // XHR patch abort
    prototype.abort = function () {
      if (isRelativeOrProxyUrl(this._url)) {
        // @ts-ignore
        return window.WabXMLHttpRequest.abort.call(this);
      }
      this.readyState = 0;
      setTimeout(() => {
        this.dispatchEvent(new Event("abort"));
        this.dispatchEvent(new Event("loadend"));
      });
    };

    // XHR patch open
    prototype.open = function (method: string, url: string) {
      this._method = method.toLocaleUpperCase();
      this._url = url;

      if (
        !this._method ||
        this._method === "GET" ||
        this._method === "HEAD" ||
        this._method === "OPTIONS" ||
        this._method === "TRACE"
      ) {
        if (isRelativeOrProxyUrl(url)) {
          // @ts-ignore
          return window.WabXMLHttpRequest.open.call(this, method, url);
        }

        this._url = createProxyUrl(this._url);
        // @ts-ignore
        return window.WabXMLHttpRequest.open.call(this, method, this._url);
      }
      Object.defineProperties(this, {
        readyState: {
          get: function () {
            return this._readyState ?? 0;
          },
          set: function (val: number) {
            this._readyState = val;
            setTimeout(() => {
              this.dispatchEvent(new Event("readystatechange"));
            });
          },
        },
      });
      setTimeout(() => {
        this.dispatchEvent(new Event("loadstart"));
      });
      this.readyState = 1;
    };

    // XHR patch set request header
    prototype.setRequestHeader = function (header: string, value: string) {
      // a workaround for the following android web view issue:
      // https://issues.chromium.org/issues/40450316
      // Sets the user-agent header to a custom value so that its not stripped
      // on its way to the native layer
      if (header === "User-Agent" || header === "user-agent") {
        header = "x-cap-user-agent";
      }

      if (isRelativeOrProxyUrl(this._url)) {
        // @ts-ignore
        return window.WabXMLHttpRequest.setRequestHeader.call(
          this,
          header,
          value
        );
      }
      this._headers[header] = value;
    };

    // XHR patch send
    prototype.send = function (body?: Document | XMLHttpRequestBodyInit) {
      if (isRelativeOrProxyUrl(this._url)) {
        // @ts-ignore
        return window.WabXMLHttpRequest.send.call(this, body);
      }

      const tag = `CapacitorHttp XMLHttpRequest ${Date.now()} ${this._url}`;
      console.time(tag);

      try {
        this.readyState = 2;

        Object.defineProperties(this, {
          response: {
            value: "",
            writable: true,
          },
          responseText: {
            value: "",
            writable: true,
          },
          responseURL: {
            value: "",
            writable: true,
          },
          status: {
            value: 0,
            writable: true,
          },
        });
        // @ts-ignore
        convertBody(body, "application/json").then(
          ({ data, type, headers }) => {
            const otherHeaders =
              this._headers != null && Object.keys(this._headers).length > 0
                ? this._headers
                : undefined;

            // intercept request & pass to the bridge
            // @ts-ignore
            Wab.callFunction({
              name: "http",
              params: {
                action: "request",
                url: this._url,
                method: this._method,
                data: data !== null ? data : undefined,
                headers: {
                  ...headers,
                  ...otherHeaders,
                },
                dataType: type,
              },
            })
              .then((nativeResponse: any) => {
                // intercept & parse response before returning
                if (this.readyState == 2) {
                  //TODO: Add progress event emission on native side
                  if (isProgressEventAvailable()) {
                    this.dispatchEvent(
                      new ProgressEvent("progress", {
                        lengthComputable: true,
                        loaded: nativeResponse.data.length,
                        total: nativeResponse.data.length,
                      })
                    );
                  }
                  this._headers = nativeResponse.headers;
                  this.status = nativeResponse.status;
                  if (
                    this.responseType === "" ||
                    this.responseType === "text"
                  ) {
                    this.response =
                      typeof nativeResponse.data !== "string"
                        ? JSON.stringify(nativeResponse.data)
                        : nativeResponse.data;
                  } else {
                    this.response = nativeResponse.data;
                  }
                  this.responseText = (
                    nativeResponse.headers["Content-Type"] ||
                    nativeResponse.headers["content-type"]
                  )?.startsWith("application/json")
                    ? JSON.stringify(nativeResponse.data)
                    : nativeResponse.data;
                  this.responseURL = nativeResponse.url;
                  this.readyState = 4;
                  setTimeout(() => {
                    this.dispatchEvent(new Event("load"));
                    this.dispatchEvent(new Event("loadend"));
                  });
                }
                console.timeEnd(tag);
              })
              .catch((error: any) => {
                this.status = error.status;
                this._headers = error.headers;
                this.response = error.data;
                this.responseText = JSON.stringify(error.data);
                this.responseURL = error.url;
                this.readyState = 4;
                if (isProgressEventAvailable()) {
                  this.dispatchEvent(
                    new ProgressEvent("progress", {
                      lengthComputable: false,
                      loaded: 0,
                      total: 0,
                    })
                  );
                }
                setTimeout(() => {
                  this.dispatchEvent(new Event("error"));
                  this.dispatchEvent(new Event("loadend"));
                });
                console.timeEnd(tag);
              });
          }
        );
      } catch (error: any) {
        this.status = 500;
        this._headers = {};
        this.response = error;
        this.responseText = error.toString();
        this.responseURL = this._url;
        this.readyState = 4;
        if (isProgressEventAvailable()) {
          this.dispatchEvent(
            new ProgressEvent("progress", {
              lengthComputable: false,
              loaded: 0,
              total: 0,
            })
          );
        }
        setTimeout(() => {
          this.dispatchEvent(new Event("error"));
          this.dispatchEvent(new Event("loadend"));
        });
        console.timeEnd(tag);
      }
    };

    // XHR patch getAllResponseHeaders
    prototype.getAllResponseHeaders = function () {
      if (isRelativeOrProxyUrl(this._url)) {
        // @ts-ignore
        return window.WabXMLHttpRequest.getAllResponseHeaders.call(this);
      }

      let returnString = "";
      for (const key in this._headers) {
        if (key != "Set-Cookie") {
          returnString += key + ": " + this._headers[key] + "\r\n";
        }
      }
      return returnString;
    };

    // XHR patch getResponseHeader
    prototype.getResponseHeader = function (name: string) {
      if (isRelativeOrProxyUrl(this._url)) {
        // @ts-ignore
        return window.WabXMLHttpRequest.getResponseHeader.call(this, name);
      }
      return this._headers[name];
    };

    Object.setPrototypeOf(xhr, prototype);
    return xhr;
  } as unknown as PatchedXMLHttpRequestConstructor;
  // @ts-ignore
  Object.assign(window.XMLHttpRequest, window.WabXMLHttpRequest.fullObject);
}
